feat: add validation to PartialMmr deserialization and from_parts#812
feat: add validation to PartialMmr deserialization and from_parts#812Farukest wants to merge 4 commits into0xMiden:nextfrom
PartialMmr deserialization and from_parts#812Conversation
61461bb to
0724921
Compare
| ) -> Result<Self, MmrError> { | ||
| let forest = peaks.forest(); | ||
|
|
||
| // Validate track_latest: can only be true if forest has an odd element (single leaf tree) |
There was a problem hiding this comment.
The new check rejects inputs where track_latest=true but the forest has no single-leaf tree.
This breaks old data that set the flag loosely.
There was a problem hiding this comment.
This validation is intentional for security (untrusted deserialization from #802).
If track_latest=true but there's no single-leaf tree, the data is logically inconsistent, there's no "latest leaf" to track.
For trusted sources or migration paths, from_parts_unchecked() is available which skips all validation.
Should we add a note in the docs about this breaking change for invalid data ?
There was a problem hiding this comment.
We should perhaps just mark the change as breaking in the Changelog.
| let result = PartialMmr::from_parts(peaks_even.clone(), BTreeMap::new(), false); | ||
| assert!(result.is_ok()); | ||
|
|
||
| // Invalid case: node index out of bounds |
There was a problem hiding this comment.
The test only checks leaf nodes. Please add tests for:
- Index 0 (invalid)
- Large even indices beyond the forest range
- Bad inputs during deserialization
These matter because internal nodes are used for auth paths.
2f03f9f to
d9d8176
Compare
|
All done @huitseeker. kindly need review again. |
huitseeker
left a comment
There was a problem hiding this comment.
This is shaping up, thanks!
| // For an empty forest, no nodes are valid | ||
| if !nodes.is_empty() && forest.is_empty() { | ||
| return Err(MmrError::InconsistentPartialMmr( | ||
| "nodes present but forest is empty".into(), |
There was a problem hiding this comment.
The current validation only checks that indices are within rightmost_in_order_index(), but this includes "separator" positions between trees in multi-tree forests. For example, in a forest with 7 leaves (0b111), index 6 falls between trees but would pass validation even though it does not correspond to a real MMR node.
These separator indices are not valid for NodeMap. See:
track()only inserts atidx.sibling()whereidxstarts from a valid leaf positionadd()only inserts at positions derived fromrightmost_in_order_index()and its sibling- The
NodeMapdocumentation states it contains "authentication nodes" which are always real tree nodes, never separators
Consider adding a Forest::is_valid_in_order_index() helper that checks if an index falls within an actual tree span rather than a separator gap.
| let result = PartialMmr::from_parts(peaks.clone(), BTreeMap::new(), true); | ||
| assert!(result.is_ok()); | ||
|
|
||
| // Build an MMR with 8 leaves (no single leaf tree: 0b1000) |
There was a problem hiding this comment.
The tests don't cover the case where an index is in-range but points to a separator between trees. See the comment above on that topic.
| assert!(result.is_err()); | ||
|
|
||
| // Invalid case: index 0 (which is never valid for InOrderIndex) | ||
| let mut nodes_with_zero = BTreeMap::new(); |
There was a problem hiding this comment.
The comment says the rightmost in-order index for 7 leaves is 12, but Forest::new(0b0111).rightmost_in_order_index() returns 13. The test at line 191 in tests.rs confirms this. Please fix or remove the comment to avoid confusion.
| ) -> Result<Self, MmrError> { | ||
| let forest = peaks.forest(); | ||
|
|
||
| // Validate track_latest: can only be true if forest has an odd element (single leaf tree) |
There was a problem hiding this comment.
We should perhaps just mark the change as breaking in the Changelog.
|
All done @huitseeker 👍 |
This commit adds validation to `PartialMmr::from_parts()` and the `Deserializable` implementation to ensure consistency between components: - Validates that `track_latest` is only true when forest has a single leaf tree - Validates that all node indices are within forest bounds - Adds `from_parts_unchecked()` for performance-critical trusted code paths - Updates `Deserializable` to use the validating constructor This addresses security concerns when deserializing from untrusted sources. Closes 0xMiden#802
Address review feedback: - Reject index 0 as invalid (InOrderIndex starts at 1) - Check all indices against forest.rightmost_in_order_index() - Handle empty forest case explicitly - Add tests for index 0, large even indices, and deserialization
9f3ce04 to
fbd1ce9
Compare
- Add Forest::is_valid_in_order_index() to check if an index points to an actual node (not a separator position between trees) - Update from_parts() to reject separator indices - Add tests for separator index validation (indices 8 and 12 for 7-leaf forest) - Fix comment: rightmost in-order index for 7 leaves is 13, not 12 - Mark PR as [BREAKING] in CHANGELOG
fbd1ce9 to
f78b142
Compare
| // Single tree forests (power of 2 leaves) have no separators | ||
| // Forest with 1 leaf: valid indices are just 1 | ||
| let forest_1 = Forest::new(0b0001); | ||
| assert!(!forest_1.is_valid_in_order_index(&idx(2)), "index 0 is invalid"); |
There was a problem hiding this comment.
| assert!(!forest_1.is_valid_in_order_index(&idx(2)), "index 0 is invalid"); | |
| assert!(!forest_1.is_valid_in_order_index(&idx(2)), "index 2 is invalid"); |
krushimir
left a comment
There was a problem hiding this comment.
Left a few minor nits but nothing blocking. LGTM!
|
|
||
| /// Returns a new [PartialMmr] instantiated from the specified components without validation. | ||
| /// | ||
| /// # Safety |
There was a problem hiding this comment.
nit: # Safety is conventionally used for unsafe fn in Rust docs. Since this is a safe function, maybe # Correctness or # Preconditions fits better?
| /// - All tracked leaf positions must be within forest bounds. | ||
| /// - All tracked leaves must have their values in the nodes map. | ||
| /// - All node indices must be valid leaf or internal node positions within the forest. | ||
| /// |
There was a problem hiding this comment.
nit: it might be worth noting in the doc that this is structural validation only. Just so callers don't assume Ok means every tracked leaf is openable via open().
Something like:
/// Note: This performs structural validation only. It does not verify that authentication
/// paths for tracked leaves are complete or that node values are cryptographically
/// consistent with the peaks.
Summary
PartialMmr::from_parts()to ensure consistency between componentsfrom_parts_unchecked()for performance-critical trusted code pathsDeserializableimplementation to use the validating constructorValidation Checks
track_latestcan only betruewhen forest has a single leaf treeNote
PartialMmr::from_partsandDeserializabledid not validate consistency between components. This is a security concern when deserializing from untrusted sources (e.g., viaProvenTransaction).Changes
InconsistentPartialMmrerror variantfrom_parts()now returnsResult<Self, MmrError>with validationfrom_parts_unchecked()for trusted/performance-critical pathsDeserializablenow validates viaMmrPeaks::new()andfrom_parts()Closes #802